import html2canvas from 'html2canvas'
import JsPDF from 'jspdf'

/**
 * 参数类型
 */
type OptionsType = {
  /**
   * pdf名称
   */
  name: string
  /**
   * 分辨率
   */
  scale?: number
  /**
   * 参与高度计算的元素块的
   * classname
   */
  blockName: string
  /**
   * 需要跳到新的一页的元素块的 classname
   */
  jumpToNextPageClassName?: string
  /**
   * 是否批量下载
   */
  immediateDownload?: boolean
}

/**
 * 获取元素计算属性
 * @param element 元素
 * @param attr 属性
 * @returns array
 */
const getElementStyle = <T extends HTMLElement, K extends keyof CSSStyleDeclaration>(element: T, attr: K) => {
  const ele = window.getComputedStyle(element, null)
  const style = ele ? ele[attr] : ''
  // console.log(element, style)
  // 匹配字符串中的数字，比如 a='30px -50px' 或者 b='20px'
  const exec = /-?\d+(\.\d+)?(?=px)/g
  const value = typeof style === 'string' ? style.match(exec) : [0]
  return value ? value.map(v => +v) : [0]
}

/**
 * 检测某元素是否包含指定的 className
 * @param element
 * @param className
 */
const hasClassName = (element: HTMLElement, className: string) => {
  const reg = new RegExp(`(^|\\s)${className}(\\s|$)`)
  return reg.test(element.className)
}

/**
 * 检测某元素是否有绝对定位
 * @param element
 */
const isAbsolute = (element: HTMLElement) => {
  const position = window.getComputedStyle(element).position
  return position === 'absolute' || position === 'fixed'
}

/**
 * 分页计算,并返回每页的 y 轴偏移量
 * @param ele
 * @param canvasWidth       canvas宽度
 * @param singlePageHeight  单页canvas高度
 * @param blockName  参与高度计算的元素块的 classname
 * @param jumpToNextPageClassName  需要跳到新的一页的元素块的 classname
 */
const computedPaging = (
  ele: HTMLElement,
  canvasWidth: number,
  singlePageHeight: number,
  blockName: string,
  jumpToNextPageClassName?: string
) => {
  // 页面宽度
  const eleW = ele.offsetWidth
  // 缩放比: html / canvas
  const htmlScale = eleW / canvasWidth
  // 每页html的高度
  const singleHtmlHeight = singlePageHeight * htmlScale

  const result: number[] = []

  // pdf.addImage 在y方向的偏移量
  let startY = 0
  // 单页可截取的html元素高度
  let cutHeight = 0
  // 底部可留白30高度
  const maxHeight = singleHtmlHeight - 30

  // 添加坐标
  const pushStartY = (y: number) => {
    startY += y * (1 / htmlScale)
    console.log('cutHeight: ', y)
    result.push(startY)
  }
  // 初始坐标：0
  pushStartY(cutHeight)

  // 遍历元素块
  const deepFind = (node: HTMLElement) => {
    const marginTop = getElementStyle(node, 'marginTop')[0]
    const marginBottom = getElementStyle(node, 'marginBottom')[0]

    const nodeHeight = node.offsetHeight + marginTop
    const sumHeight = nodeHeight + cutHeight

    /**
     * node 元素块的高度进行累加后，如果累加高度超出了单页高度，就去找它的子元素块
     * 若没有超出单页高度，就继续找 node 元素块的兄弟元素块，直到累加高度等于或者超出单页高度
     */
    if (sumHeight > singleHtmlHeight) {
      const paddingAndBorderTop = getElementStyle(node, 'paddingTop')[0] + getElementStyle(node, 'borderTop')[0]
      const paddingAndBorderBottom = getElementStyle(node, 'paddingBottom')[0] + getElementStyle(node, 'borderBottom')[0]

      cutHeight += paddingAndBorderTop
      // 遍历子节点
      const childNodes = Array.from(node.children) as HTMLElement[]
      for (let i = 0, len = childNodes.length; i < len; i++) {
        const child = childNodes[i]
        if (hasClassName(child, blockName)) {
          // 排除绝对定位的元素，脱离了文档流，对高度计算没有贡献
          if (isAbsolute(child)) {
            continue
          }
          // 有 download-pdf__new__page 表示此元素需要放到新的一页
          if (jumpToNextPageClassName && hasClassName(child, jumpToNextPageClassName)) {
            cutHeight -= paddingAndBorderTop

            // 存储y偏移量
            pushStartY(cutHeight)

            // 重新开始累加高度
            cutHeight = marginBottom
            deepFind(child)
          } else {
            deepFind(child)
            // 把子元素遍历完之后，要把父容器的 paddingAndBorderBottom 和 marginBottom 加上
            if (i === len - 1) {
              cutHeight += paddingAndBorderBottom + marginBottom
            }
          }
        } else {
          // 元素A的第一个子节点没有className
          // 说明元素A不能拆分，只能计算整体高度，并且需要把A放在下一页，此时，元素A的上一个兄弟元素是临界元素。
          // 此时，累加的元素A的 paddingAndBorderTop 需要减去，留到下一页去参与计算
          cutHeight -= i === 0 ? paddingAndBorderTop : 0

          // 存储y偏移量
          pushStartY(cutHeight)

          // 重新开始累加高度
          cutHeight = nodeHeight + marginBottom
          break
        }
      }
      return
    } else if (sumHeight >= maxHeight) {
      pushStartY(sumHeight)

      // 重置叠加高度
      cutHeight = 0
      return
    } else {
      cutHeight += nodeHeight + marginBottom
      return
    }
  }
  deepFind(ele)
  return result
}

/**
 * 初始化canvas
 * @param ele
 */
const initCanvas = (ele: HTMLElement): HTMLCanvasElement => {
  const scale = 1
  const eleW = ele.offsetWidth // 获得该容器的宽
  const eleH = ele.offsetHeight // 获得该容器的高
  const canvas = document.createElement('canvas')

  canvas.width = eleW * scale // 将画布宽&&高放大 scale 倍
  canvas.height = eleH * scale

  const context = canvas.getContext('2d') as CanvasRenderingContext2D
  context.scale(scale, scale)

  return canvas
}

/**
 * 将指定位置区域的元素转换为图片数据
 * @param ele
 * @param _dpiScale
 * @param y
 * @param height
 */
const html2ImageData = async (ele: HTMLElement, _dpiScale: number, y?: number, height?: number) => {
  /**
   * scale: 分辨率，越大越清晰
   * useCORS: 允许 canvas 画布内 可以跨域请求外部链接图片, 允许跨域请求。
   */
  const canvas = await html2canvas(ele, { scale: _dpiScale, height, y, useCORS: true })
  // 图片数据
  return canvas.toDataURL('image/jpeg', 1.0)
}

/**
 * 生成PDF
 * @param ele 要生成 pdf 的DOM元素（容器）
 * @param options
 */
export const downloadPDF = async (
  ele: HTMLElement,
  { name: pdfName, scale = 1, blockName, jumpToNextPageClassName, immediateDownload = false }: OptionsType
): Promise<any> => {
  // 分辨率
  const _dpiScale = window.devicePixelRatio * scale
  // 初始化canvas
  const canvas = initCanvas(ele)

  // canvas 画布内容的宽高
  const canvasWidth = canvas.width
  const canvasHeight = canvas.height

  // 初始化 JsPDF
  const pdf = new JsPDF('portrait', 'pt', 'a4')
  // 纸张的宽度和高度 a4 纸的尺寸[595.28,841.89]
  const paperSize = {
    width: pdf.internal.pageSize.getWidth(),
    height: pdf.internal.pageSize.getHeight()
  }

  // 页面缩放比: canvas : pdf
  const pageScale = canvasWidth / paperSize.width
  // 单页pdf中，可渲染的 canvas 高度
  const singlePageHeight = paperSize.height * pageScale

  // canvas 内容页总高
  const totalHeight = canvasHeight

  // pdf中图片的宽和总高
  const imgWidth = paperSize.width
  const imgHeight = (paperSize.width / canvasWidth) * canvasHeight

  // 有两个高度需要区分，一个是html页面的实际高度，和生成pdf的页面高度(841.89)
  // 当内容未超过pdf一页显示的范围，无需分页
  if (totalHeight < singlePageHeight) {
    // 在pdf.addImage(pageData, 'JPEG', 左，上，宽度，高度)设置在pdf中显示；
    const pageData = await html2ImageData(ele, _dpiScale)
    pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, imgHeight)
  } else {
    // 进行分页计算
    const result = computedPaging(ele, canvasWidth, singlePageHeight, blockName, jumpToNextPageClassName)
    console.log(result)
    let index = 0
    const length = result.length
    // 分页
    while (index < length) {
      // 页面y方向的偏移量
      const positionY = result[index]
      index += 1
      console.log('positionY: ', positionY)
      const height = result[index] ? result[index] - positionY : totalHeight - positionY
      console.log('height: ', height)
      const pageData = await html2ImageData(ele, _dpiScale, positionY, height)
      const pdfH = height ? height / pageScale : paperSize.height
      console.log('pdfH', pdfH)
      pdf.addImage(pageData, 'JPEG', 0, 0, imgWidth, pdfH)

      // 注：此处用于解决多页情况下，尾页始终多一个空白页的问题
      if (index < length) {
        pdf.addPage()
      }
    }
  }
  // 可动态生成
  if (immediateDownload) {
    return pdf.output('blob')
  } else {
    pdf.save(pdfName)
    return ''
  }
}
